package io.noties.markwon.plugin.typogram.bean

/*
 * DiTAA - Diagrams Through Ascii Art
 *
 * Copyright (C) 2004 Efstathios Sideris
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */
import android.graphics.Color
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.util.Stack
import java.util.regex.Pattern
import kotlin.math.abs
import kotlin.math.floor


/**
 *
 * @author Efstathios Sideris
 */
open class TextGrid {
    /**
     * @return
     */
    protected var rows: ArrayList<StringBuffer>
        private set

    fun addToMarkupTags(tags: Collection<String>) {
        markupTags.addAll(tags)
    }

    constructor() {
        rows = ArrayList()
    }

    constructor(width: Int, height: Int) {
        val space: String =" ".repeat(width)
        rows = ArrayList()
        for (i in 0 until height) rows.add(StringBuffer(space))
    }

    constructor(otherGrid: TextGrid) {
        rows = ArrayList()
        for (row: StringBuffer? in otherGrid.rows) {
            rows.add(StringBuffer(row))
        }
    }

    fun clear() {
        val blank: String = " ".repeat(width)
        val height = height
        rows.clear()
        for (i in 0 until height) rows.add(StringBuffer(blank))
    }

    //	duplicated code due to lots of hits to this function
    operator fun get(x: Int, y: Int): Char {
        return if ((x > width - 1) || (y > height - 1) || (x < 0) || (y < 0)) 0.toChar() else rows[y][x]
    }

    //duplicated code due to lots of hits to this function
    operator fun get(cell: Cell): Char {
        return if ((cell.x > width - 1
                    ) || (cell.y > height - 1
                    ) || (cell.x < 0
                    ) || (cell.y < 0)
        ) 0.toChar() else rows[cell.y][cell.x]
    }

    fun getRow(y: Int): StringBuffer {
        return rows[y]
    }

    fun getSubGrid(x: Int, y: Int, width: Int, height: Int): TextGrid {
        val grid = TextGrid(width, height)
        for (i in 0 until height) {
            grid.setRow(i, StringBuffer(getRow(y + i).subSequence(x, x + width)))
        }
        return grid
    }

    fun getTestingSubGrid(cell: Cell): TextGrid {
        return getSubGrid(cell.x - 1, cell.y - 1, 3, 3)
    }

    fun getStringAt(x: Int, y: Int, length: Int): String? {
        return getStringAt(Cell(x, y), length)
    }

    fun getStringAt(cell: Cell, length: Int): String? {
        val x = cell.x
        val y = cell.y
        return if ((x > width - 1
                    ) || (y > height - 1
                    ) || (x < 0
                    ) || (y < 0)
        ) null else rows[y].substring(x, x + length)
    }

    fun getNorthOf(x: Int, y: Int): Char {
        return get(x, y - 1)
    }

    fun getSouthOf(x: Int, y: Int): Char {
        return get(x, y + 1)
    }

    fun getEastOf(x: Int, y: Int): Char {
        return get(x + 1, y)
    }

    fun getWestOf(x: Int, y: Int): Char {
        return get(x - 1, y)
    }

    fun getNorthOf(cell: Cell): Char {
        return getNorthOf(cell.x, cell.y)
    }

    fun getSouthOf(cell: Cell): Char {
        return getSouthOf(cell.x, cell.y)
    }

    fun getEastOf(cell: Cell): Char {
        return getEastOf(cell.x, cell.y)
    }

    fun getWestOf(cell: Cell): Char {
        return getWestOf(cell.x, cell.y)
    }

    fun writeStringTo(x: Int, y: Int, str: String) {
        writeStringTo(Cell(x, y), str)
    }

    fun writeStringTo(cell: Cell, str: String) {
        if (isOutOfBounds(cell)) return
        rows[cell.y].replace(cell.x, cell.x + str.length, str)
    }

    operator fun set(cell: Cell, c: Char) {
        set(cell.x, cell.y, c)
    }

    operator fun set(x: Int, y: Int, c: Char) {
        if (x > width - 1 || y > height - 1) return
        val row = rows[y]
        row.setCharAt(x, c)
    }

    fun setRow(y: Int, row: String) {
        if (y > height || row.length != width) throw IllegalArgumentException("setRow out of bounds or string wrong size")
        rows[y] = StringBuffer(row)
    }

    fun setRow(y: Int, row: StringBuffer) {
        if (y > height || row.length != width) throw IllegalArgumentException("setRow out of bounds or string wrong size")
        rows[y] = row
    }

    //empty buffer
    val width: Int
        get() {
            return if (rows.size == 0) 0 else rows[0].length //empty buffer
        }

    val height: Int
        get() = rows.size

    fun printDebug() {
        val it: Iterator<StringBuffer> = rows.iterator()
        var i = 0
        println("    " + "0123456789".repeat(floor((width / 10).toDouble()).toInt() + 1))
        while (it.hasNext()) {
            val row = it.next().toString()
            var index = i.toString()
            if (i < 10) index = " $index"
            println("$index ($row)")
            i++
        }
    }

    val debugString: String
        get() {
            val buffer = StringBuffer()
            val it: Iterator<StringBuffer> = rows.iterator()
            var i = 0
            buffer.append(("    " + "0123456789".repeat((floor((width / 10).toDouble()) + 1).toInt()) + "\n"))
            while (it.hasNext()) {
                var row = it.next().toString()
                var index = i.toString()
                if (i < 10) index = " $index"
                row = row.replace("\n".toRegex(), "\\\\n")
                row = row.replace("\r".toRegex(), "\\\\r")
                buffer.append("$index ($row)\n")
                i++
            }
            return buffer.toString()
        }

    override fun toString(): String {
        return debugString
    }

    /**
     * Adds grid to this. Space characters in this grid
     * are replaced with the corresponding contents of
     * grid, otherwise the contents are unchanged.
     *
     * @param grid
     * @return false if the grids are of different size
     */
    fun add(grid: TextGrid): Boolean {
        if ((width != grid.width
                    || height != grid.height)
        ) return false
        val width = width
        val height = height
        for (yi in 0 until height) {
            for (xi in 0 until width) {
                if (get(xi, yi) == ' ') set(xi, yi, grid[xi, yi])
            }
        }
        return true
    }

    /**
     * Replaces letters or numbers that are on horizontal or vertical
     * lines, with the appropriate character that will make the line
     * continuous (| for vertical and - for horizontal lines)
     *
     */
    fun replaceTypeOnLine() {
        val width = width
        val height = height
        for (yi in 0 until height) {
            for (xi in 0 until width) {
                val c = get(xi, yi)
                if (Character.isLetterOrDigit(c)) {
                    val isOnHorizontalLine = isOnHorizontalLine(xi, yi)
                    val isOnVerticalLine = isOnVerticalLine(xi, yi)
                    if (isOnHorizontalLine && isOnVerticalLine) {
                        set(xi, yi, '+')
                        if (DEBUG) println("replaced type on line '$c' with +")
                    } else if (isOnHorizontalLine) {
                        set(xi, yi, '-')
                        if (DEBUG) println("replaced type on line '$c' with -")
                    } else if (isOnVerticalLine) {
                        set(xi, yi, '|')
                        if (DEBUG) println("replaced type on line '$c' with |")
                    }
                }
            }
        }
    }

    fun replacePointMarkersOnLine() {
        val width = width
        val height = height
        for (yi in 0 until height) {
            for (xi in 0 until width) {
                val c = get(xi, yi)
                val cell: Cell = Cell(xi, yi)
                if (pointMarkers.contains(c) && isStarOnLine(cell)) {
                    var isOnHorizontalLine = false
                    if (horizontalLines.contains(get(cell.east)))
                        isOnHorizontalLine = true
                    if (horizontalLines.contains(get(cell.west)))
                        isOnHorizontalLine = true
                    var isOnVerticalLine = false
                    if (verticalLines.contains(get(cell.north)))
                        isOnVerticalLine = true
                    if (verticalLines.contains(get(cell.south)))
                        isOnVerticalLine = true
                    if (isOnHorizontalLine && isOnVerticalLine) {
                        set(xi, yi, '+')
                        if (DEBUG) println("replaced marker on line '$c' with +")
                    } else if (isOnHorizontalLine) {
                        set(xi, yi, '-')
                        if (DEBUG) println("replaced marker on line '$c' with -")
                    } else if (isOnVerticalLine) {
                        set(xi, yi, '|')
                        if (DEBUG) println("replaced marker on line '$c' with |")
                    }
                }
            }
        }
    }

    val pointMarkersOnLine: CellSet
        get() {
            val result = CellSet()
            val width = width
            val height = height
            for (yi in 0 until height) {
                for (xi in 0 until width) {
                    val c = get(xi, yi)
                    if ( pointMarkers.contains(c) && isStarOnLine(Cell(xi, yi))) {
                        result.add(Cell(xi, yi))
                    }
                }
            }
            return result
        }

    fun replaceHumanColorCodes() {
        val height = height
        for (y in 0 until height) {
            var row = rows[y].toString()
            val it: Iterator<*> = humanColorCodes.keys.iterator()
            while (it.hasNext()) {
                var humanCode = it.next() as String
                var hexCode = humanColorCodes[humanCode]
                if (hexCode != null) {
                    humanCode = "c$humanCode"
                    hexCode = "c$hexCode"
                    row = row.replace(humanCode.toRegex(), hexCode)
                    rows[y] =
                        StringBuffer(row) //TODO: this is not the most efficient way to do this
                    row = rows[y].toString()
                }
            }
        }
    }

    /**
     * Replace all occurences of c1 with c2
     *
     * @param c1
     * @param c2
     */
    fun replaceAll(c1: Char, c2: Char) {
        val width = width
        val height = height
        for (yi in 0 until height) {
            for (xi in 0 until width) {
                val c = get(xi, yi)
                if (c == c1) set(xi, yi, c2)
            }
        }
    }

    fun hasBlankCells(): Boolean {
        val set = CellSet()
        val width = width
        val height = height
        for (y in 0 until height) {
            for (x in 0 until width) {
                val cell: Cell = Cell(x, y)
                if (isBlank(cell)) return true
            }
        }
        return false
    }

    val allNonBlank: CellSet
        get() {
            val set = CellSet()
            val width = width
            val height = height
            for (y in 0 until height) {
                for (x in 0 until width) {
                    val cell: Cell = Cell(x, y)
                    if (!isBlank(cell)) set.add(cell)
                }
            }
            return set
        }
    val allBoundaries: CellSet
        get() {
            val set = CellSet()
            val width = width
            val height = height
            for (y in 0 until height) {
                for (x in 0 until width) {
                    val cell: Cell = Cell(x, y)
                    if (isBoundary(cell)) set.add(cell)
                }
            }
            return set
        }
    val allBlanksBetweenCharacters: CellSet
        get() {
            val set = CellSet()
            val width = width
            val height = height
            for (y in 0 until height) {
                for (x in 0 until width) {
                    val cell: Cell = Cell(x, y)
                    if (isBlankBetweenCharacters(cell)) set.add(cell)
                }
            }
            return set
        }

    /**
     * Returns an ArrayList of CellStringPairs that
     * represents all the continuous (non-blank) Strings
     * in the grid. Used on buffers that contain only
     * type, in order to find the positions and the
     * contents of the strings.
     *
     * @return
     */
    fun findStrings(): ArrayList<CellStringPair> {
        val result = ArrayList<CellStringPair>()
        val width = width
        val height = height
        for (y in 0 until height) {
            var x = 0
            while (x < width) {
                if (!isBlank(x, y)) {
                    val start: Cell = Cell(x, y)
                    var str: String = get(x, y).toString()
                    var c = get(++x, y)
                    var finished = false
                    //while(c != ' '){
                    while (!finished) {
                        str += c.toString()
                        c = get(++x, y)
                        val next = get(x + 1, y)
                        if ((c == ' ' || c.code == 0) && (next == ' ' || next.code == 0)) finished =
                            true
                    }
                    result.add(CellStringPair(start, str))
                }
                x++
            }
        }
        return result
    }

    /**
     * This is done in a bit of a messy way, should be impossible
     * to go out of sync with corresponding GridPatternGroup.
     *
     * @param cell
     * @param entryPointId
     * @return
     */
    fun hasEntryPoint(cell: Cell, entryPointId: Int): Boolean {
        val result = ""
        val c = get(cell)
        if (entryPointId == 1) {
            return  entryPoints1.contains(c)
        } else if (entryPointId == 2) {
            return  entryPoints2.contains(c)
        } else if (entryPointId == 3) {
            return  entryPoints3.contains(c)
        } else if (entryPointId == 4) {
            return  entryPoints4.contains(c)
        } else if (entryPointId == 5) {
            return  entryPoints5.contains(c)
        } else if (entryPointId == 6) {
            return  entryPoints6.contains(c)
        } else if (entryPointId == 7) {
            return  entryPoints7.contains(c)
        } else if (entryPointId == 8) {
            return  entryPoints8.contains(c)
        }
        return false
    }

    /**
     * true if cell is blank and the east and west cells are not
     * (used to find gaps between words)
     *
     * @param cell
     * @return
     */
    fun isBlankBetweenCharacters(cell: Cell): Boolean {
        return ((isBlank(cell)
                && !isBlank(cell.east)
                && !isBlank(cell.west)))
    }

    /**
     * Makes blank all the cells that contain non-text
     * elements.
     */
    fun removeNonText() {
        //the following order is significant
        //since the south-pointing arrowheads
        //are determined based on the surrounding boundaries
        removeArrowheads()
        removeColorCodes()
        removeBoundaries()
        removeMarkupTags()
    }

    fun removeArrowheads() {
        val width = width
        val height = height
        for (yi in 0 until height) {
            for (xi in 0 until width) {
                val cell: Cell = Cell(xi, yi)
                if (isArrowhead(cell)) set(cell, ' ')
            }
        }
    }

    fun removeColorCodes() {
        val cells: Iterator<*> = findColorCodes().iterator()
        while (cells.hasNext()) {
            var cell = (cells.next() as CellColorPair).cell
            set(cell, ' ')
            cell = cell.east
            set(cell, ' ')
            cell = cell.east
            set(cell, ' ')
            cell = cell.east
            set(cell, ' ')
        }
    }

    fun removeBoundaries() {
        val toBeRemoved: ArrayList<Cell> = ArrayList<Cell>()
        val width = width
        val height = height
        for (yi in 0 until height) {
            for (xi in 0 until width) {
                val cell: Cell = Cell(xi, yi)
                if (isBoundary(cell)) toBeRemoved.add(cell)
            }
        }

        //remove in two stages, because decision of
        //isBoundary depends on contants of surrounding
        //cells
        val it: Iterator<*> = toBeRemoved.iterator()
        while (it.hasNext()) {
            val cell = it.next() as Cell
            set(cell, ' ')
        }
    }

    fun findArrowheads(): ArrayList<Cell> {
        val result: ArrayList<Cell> = ArrayList<Cell>()
        val width = width
        val height = height
        for (yi in 0 until height) {
            for (xi in 0 until width) {
                val cell: Cell = Cell(xi, yi)
                if (isArrowhead(cell)) result.add(cell)
            }
        }
        if (DEBUG) println(result.size.toString() + " arrowheads found")
        return result
    }

    fun findColorCodes(): ArrayList<CellColorPair> {
        val colorCodePattern = Pattern.compile("c[A-F0-9]{3}")
        val result = ArrayList<CellColorPair>()
        val width = width
        val height = height
        for (yi in 0 until height) {
            for (xi in 0 until (width - 3)) {
                val cell: Cell = Cell(xi, yi)
                val s = getStringAt(cell, 4)
                val matcher = colorCodePattern.matcher(s)
                if (matcher.matches()) {
                    val cR = s!![1]
                    val cG = s[2]
                    val cB = s[3]
                    val r = Integer.valueOf(cR.toString(), 16) * 17f
                    val g = Integer.valueOf(cG.toString(), 16) * 17f
                    val b = Integer.valueOf(cB.toString(), 16) * 17f
                    result.add(CellColorPair(cell, Color.valueOf(r, g, b).toArgb()))
                }
            }
        }
        if (DEBUG) println(result.size.toString() + " color codes found")
        return result
    }

    fun findMarkupTags(): ArrayList<CellTagPair> {
        val tagPattern = Pattern.compile("\\{(.+?)\\}")
        val result = ArrayList<CellTagPair>()
        val width = width
        val height = height
        for (y in 0 until height) {
            for (x in 0 until (width - 3)) {
                val cell: Cell = Cell(x, y)
                val c = get(cell)
                if (c == '{') {
                    val rowPart = rows[y].substring(x)
                    val matcher = tagPattern.matcher(rowPart)
                    if (matcher.find()) {
                        val tagName = matcher.group(1)
                        if (markupTags.contains(tagName)) {
                            if (DEBUG) println("found tag $tagName at $x, $y")
                            result.add(CellTagPair(Cell(x, y), tagName))
                        }
                    }
                }
            }
        }
        return result
    }

    fun removeMarkupTags() {
        val it: Iterator<*> = findMarkupTags().iterator()
        while (it.hasNext()) {
            val pair = it.next() as CellTagPair
            val tagName: String = pair.tag ?: continue
            val length = 2 + tagName.length
            writeStringTo(pair.cell, " ".repeat( length))
        }
    }

    fun matchesAny(criteria: GridPatternGroup): Boolean {
        return criteria.isAnyMatchedBy(this)
    }

    fun matchesAll(criteria: GridPatternGroup): Boolean {
        return criteria.areAllMatchedBy(this)
    }

    fun matches(criteria: GridPattern): Boolean {
        return criteria.isMatchedBy(this)
    }

    fun isOnHorizontalLine(cell: Cell): Boolean {
        return isOnHorizontalLine(cell.x, cell.y)
    }

    private fun isOnHorizontalLine(x: Int, y: Int): Boolean {
        val c1 = get(x - 1, y)
        val c2 = get(x + 1, y)
        return isHorizontalLine(c1) && isHorizontalLine(c2)
    }

    fun isOnVerticalLine(cell: Cell): Boolean {
        return isOnVerticalLine(cell.x, cell.y)
    }

    private fun isOnVerticalLine(x: Int, y: Int): Boolean {
        val c1 = get(x, y - 1)
        val c2 = get(x, y + 1)
        return isVerticalLine(c1) && isVerticalLine(c2)
    }

    fun isBoundary(x: Int, y: Int): Boolean {
        return isBoundary(Cell(x, y))
    }

    fun isBoundary(cell: Cell): Boolean {
        val c = get(cell.x, cell.y)
        if (0 == c.code) return false
        if (('+' == c) || ('\\' == c) || ('/' == c)) {
            print("")
            return (isIntersection(cell)
                        || isCorner(cell)
                        || isStub(cell)
                        || isCrossOnLine(cell))
        }
        //return StringUtils.isOneOf(c, undisputableBoundaries);
        return boundaries.contains(c) && !isLoneDiagonal(cell)
    }

    fun isLine(cell: Cell): Boolean {
        return isHorizontalLine(cell) || isVerticalLine(cell)
    }

    fun isHorizontalLine(cell: Cell): Boolean {
        return isHorizontalLine(cell.x, cell.y)
    }

    fun isHorizontalLine(x: Int, y: Int): Boolean {
        val c = get(x, y)
        return if (0 == c.code) false else horizontalLines.contains(c)
    }

    fun isVerticalLine(cell: Cell): Boolean {
        return isVerticalLine(cell.x, cell.y)
    }

    fun isVerticalLine(x: Int, y: Int): Boolean {
        val c = get(x, y)
        return if (0 == c.code) false else verticalLines.contains(c)
    }

    fun isLinesEnd(x: Int, y: Int): Boolean {
        return isLinesEnd(Cell(x, y))
    }

    /**
     * Stubs are also considered end of lines
     *
     * @param cell
     * @return
     */
    fun isLinesEnd(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.linesEndCriteria)
    }

    fun isVerticalLinesEnd(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.verticalLinesEndCriteria)
    }

    fun isHorizontalLinesEnd(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.horizontalLinesEndCriteria)
    }

    fun isPointCell(cell: Cell): Boolean {
        return ((isCorner(cell)
                || isIntersection(cell)
                || isStub(cell)
                || isLinesEnd(cell)))
    }

    fun containsAtLeastOneDashedLine(set: CellSet): Boolean {
        val it: Iterator<*> = set.iterator()
        while (it.hasNext()) {
            val cell = it.next() as Cell
            if (dashedLines.contains(get(cell))) return true
        }
        return false
    }

    fun exactlyOneNeighbourIsBoundary(cell: Cell): Boolean {
        var howMany = 0
        if (isBoundary(cell.north)) howMany++
        if (isBoundary(cell.south)) howMany++
        if (isBoundary(cell.east)) howMany++
        if (isBoundary(cell.west)) howMany++
        return (howMany == 1)
    }

    /**
     *
     * A stub looks like that:
     *
     * <pre>
     *
     * +- or -+ or + or + or /- or -/ or / (you get the point)
     * |    |                |
     *
    </pre> *
     *
     * @param cell
     * @return
     */
    fun isStub(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.stubCriteria)
    }

    fun isCrossOnLine(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.crossOnLineCriteria)
    }

    fun isHorizontalCrossOnLine(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.horizontalCrossOnLineCriteria)
    }

    fun isVerticalCrossOnLine(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.verticalCrossOnLineCriteria)
    }

    fun isStarOnLine(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.starOnLineCriteria)
    }

    fun isLoneDiagonal(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.loneDiagonalCriteria)
    }

    fun isHorizontalStarOnLine(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.horizontalStarOnLineCriteria)
    }

    fun isVerticalStarOnLine(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.verticalStarOnLineCriteria)
    }

    fun isArrowhead(cell: Cell): Boolean {
        return ((isNorthArrowhead(cell)
                || isSouthArrowhead(cell)
                || isWestArrowhead(cell)
                || isEastArrowhead(cell)))
    }

    fun isNorthArrowhead(cell: Cell): Boolean {
        return get(cell) == '^'
    }

    fun isEastArrowhead(cell: Cell): Boolean {
        return get(cell) == '>'
    }

    fun isWestArrowhead(cell: Cell): Boolean {
        return get(cell) == '<'
    }

    fun isSouthArrowhead(cell: Cell): Boolean {
        return ((get(cell) == 'v' || get(cell) == 'V')
                && isVerticalLine(cell.north))
    }

    //	unicode for bullets
    //
    //	2022 bullet
    //	25CF black circle
    //	25AA black circle (small)
    //	25A0 black square
    //	25A1 white square
    //	25CB white circle
    //	25BA black right-pointing pointer
    fun isBullet(x: Int, y: Int): Boolean {
        return isBullet(Cell(x, y))
    }

    fun isBullet(cell: Cell): Boolean {
        val c = get(cell)
        return ((c == 'o' || c == '*')
                && isBlank(cell.east)
                && isBlank(cell.west)
                && Character.isLetterOrDigit(get(cell.east.east)))
    }

    fun replaceBullets() {
        val width = width
        val height = height
        for (yi in 0 until height) {
            for (xi in 0 until width) {
                val cell: Cell = Cell(xi, yi)
                if (isBullet(cell)) {
                    set(cell, ' ')
                    set(cell.east, '\u2022')
                }
            }
        }
    }

    /**
     * true if the cell is not blank
     * but the previous (west) is
     *
     * @param cell
     * @return
     */
    fun isStringsStart(cell: Cell): Boolean {
        return (!isBlank(cell) && isBlank(cell.west))
    }

    /**
     * true if the cell is not blank
     * but the next (east) is
     *
     * @param cell
     * @return
     */
    fun isStringsEnd(cell: Cell): Boolean {
        return ((!isBlank(cell) //&& (isBlank(cell.getEast()) || get(cell.getEast()) == 0));
                && isBlank(cell.east)))
    }

    fun otherStringsStartInTheSameColumn(cell: Cell): Int {
        if (!isStringsStart(cell)) return 0
        var result = 0
        val height = height
        for (y in 0 until height) {
            val cCell: Cell = Cell(cell.x, y)
            if (cCell != cell && isStringsStart(cCell)) {
                result++
            }
        }
        return result
    }

    fun otherStringsEndInTheSameColumn(cell: Cell): Int {
        if (!isStringsEnd(cell)) return 0
        var result = 0
        val height = height
        for (y in 0 until height) {
            val cCell: Cell = Cell(cell.x, y)
            if (cCell != cell && isStringsEnd(cCell)) {
                result++
            }
        }
        return result
    }

    fun isColumnBlank(x: Int): Boolean {
        val height = height
        for (y in 0 until height) {
            if (!isBlank(x, y)) return false
        }
        return true
    }

    fun followLine(x: Int, y: Int): CellSet? {
        return followLine(Cell(x, y))
    }

    fun followIntersection(cell: Cell): CellSet? {
        return followIntersection(cell, null)
    }

    fun followIntersection(cell: Cell, blocked: Cell?): CellSet? {
        if (!isIntersection(cell)) return null
        val result = CellSet()
        val cN = cell.north
        val cS = cell.south
        val cE = cell.east
        val cW = cell.west
        if (hasEntryPoint(cN, 6)) result.add(cN)
        if (hasEntryPoint(cS, 2)) result.add(cS)
        if (hasEntryPoint(cE, 8)) result.add(cE)
        if (hasEntryPoint(cW, 4)) result.add(cW)
        if (result.contains(blocked)) result.remove(blocked!!)
        return result
    }

    /**
     * Returns the neighbours of a line-cell that are boundaries
     * (0 to 2 cells are returned)
     *
     * @param cell
     * @return null if the cell is not a line
     */
    fun followLine(cell: Cell): CellSet? {
        if (isHorizontalLine(cell)) {
            val result = CellSet()
            if (isBoundary(cell.east)) result.add(cell.east)
            if (isBoundary(cell.west)) result.add(cell.west)
            return result
        } else if (isVerticalLine(cell)) {
            val result = CellSet()
            if (isBoundary(cell.north)) result.add(cell.north)
            if (isBoundary(cell.south)) result.add(cell.south)
            return result
        }
        return null
    }

    fun followLine(cell: Cell, blocked: Cell?): CellSet? {
        val nextCells: CellSet = followLine(cell)?:return null
        if (nextCells.contains(blocked)) nextCells.remove(blocked!!)
        return nextCells
    }

    fun followCorner(cell: Cell): CellSet? {
        return followCorner(cell, null)
    }

    fun followCorner(cell: Cell, blocked: Cell?): CellSet? {
        if (!isCorner(cell)) return null
        if (isCorner1(cell)) return followCorner1(cell, blocked)
        if (isCorner2(cell)) return followCorner2(cell, blocked)
        if (isCorner3(cell)) return followCorner3(cell, blocked)
        return if (isCorner4(cell)) followCorner4(cell, blocked) else null
    }

    fun followCorner1(cell: Cell): CellSet? {
        return followCorner1(cell, null)
    }

    fun followCorner1(cell: Cell, blocked: Cell?): CellSet? {
        if (!isCorner1(cell)) return null
        val result = CellSet()
        if (cell.south != blocked) result.add(cell.south)
        if (cell.east != blocked) result.add(cell.east)
        return result
    }

    fun followCorner2(cell: Cell): CellSet? {
        return followCorner2(cell, null)
    }

    fun followCorner2(cell: Cell, blocked: Cell?): CellSet? {
        if (!isCorner2(cell)) return null
        val result = CellSet()
        if (cell.south != blocked) result.add(cell.south)
        if (cell.west != blocked) result.add(cell.west)
        return result
    }

    fun followCorner3(cell: Cell): CellSet? {
        return followCorner3(cell, null)
    }

    fun followCorner3(cell: Cell, blocked: Cell?): CellSet? {
        if (!isCorner3(cell)) return null
        val result = CellSet()
        if (cell.north != blocked) result.add(cell.north)
        if (cell.west != blocked) result.add(cell.west)
        return result
    }

    fun followCorner4(cell: Cell): CellSet? {
        return followCorner4(cell, null)
    }

    fun followCorner4(cell: Cell, blocked: Cell?): CellSet? {
        if (!isCorner4(cell)) return null
        val result = CellSet()
        if (cell.north != blocked) result.add(cell.north)
        if (cell.east != blocked) result.add(cell.east)
        return result
    }

    fun followStub(cell: Cell): CellSet? {
        return followStub(cell, null)
    }

    fun followStub(cell: Cell, blocked: Cell?): CellSet? {
        if (!isStub(cell)) return null
        val result = CellSet()
        if (isBoundary(cell.east)) result.add(cell.east) else if (isBoundary(cell.west)) result.add(
            cell.west
        ) else if (isBoundary(cell.north)) result.add(cell.north) else if (isBoundary(cell.south)) result.add(
            cell.south
        )
        if (result.contains(blocked)) result.remove(blocked!!)
        return result
    }

    fun followCell(cell: Cell): CellSet? {
        return followCell(cell, null)
    }

    fun followCell(cell: Cell, blocked: Cell?): CellSet? {
        if (isIntersection(cell)) return followIntersection(cell, blocked)
        if (isCorner(cell)) return followCorner(cell, blocked)
        if (isLine(cell)) return followLine(cell, blocked)
        if (isStub(cell)) return followStub(cell, blocked)
        if (isCrossOnLine(cell)) return followCrossOnLine(cell, blocked)
        System.err.println("Umbiguous input at position $cell:")
        val subGrid = getTestingSubGrid(cell)
        subGrid.printDebug()
        throw RuntimeException("Cannot follow cell $cell: cannot determine cell type")
    }

    fun getCellTypeAsString(cell: Cell): String {
        if (isK(cell)) return "K"
        if (isT(cell)) return "T"
        if (isInverseK(cell)) return "inverse K"
        if (isInverseT(cell)) return "inverse T"
        if (isCorner1(cell)) return "corner 1"
        if (isCorner2(cell)) return "corner 2"
        if (isCorner3(cell)) return "corner 3"
        if (isCorner4(cell)) return "corner 4"
        if (isLine(cell)) return "line"
        if (isStub(cell)) return "stub"
        return if (isCrossOnLine(cell)) "crossOnLine" else "unrecognisable type"
    }

    fun followCrossOnLine(cell: Cell, blocked: Cell?): CellSet {
        val result = CellSet()
        if (isHorizontalCrossOnLine(cell)) {
            result.add(cell.east)
            result.add(cell.west)
        } else if (isVerticalCrossOnLine(cell)) {
            result.add(cell.north)
            result.add(cell.south)
        }
        if (result.contains(blocked)) result.remove(blocked!!)
        return result
    }

    fun isOutOfBounds(cell: Cell): Boolean {
        return (cell.x > width - 1
                ) || (cell.y > height - 1
                ) || (cell.x < 0
                ) || (cell.y < 0)
    }

    fun isOutOfBounds(x: Int, y: Int): Boolean {
        val c = get(x, y)
        return 0 == c.code
    }

    fun isBlank(cell: Cell): Boolean {
        val c = get(cell)
        return if (0 == c.code) false else c == ' '
    }

    fun isBlank(x: Int, y: Int): Boolean {
        val c = get(x, y)
        return if (0 == c.code) true else c == ' '
    }

    fun isCorner(cell: Cell): Boolean {
        return isCorner(cell.x, cell.y)
    }

    fun isCorner(x: Int, y: Int): Boolean {
        return (isNormalCorner(x, y) || isRoundCorner(x, y))
    }

    fun matchesAny(cell: Cell, criteria: GridPatternGroup): Boolean {
        val subGrid = getTestingSubGrid(cell)
        return subGrid.matchesAny(criteria)
    }

    fun isCorner1(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.corner1Criteria)
    }

    fun isCorner2(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.corner2Criteria)
    }

    fun isCorner3(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.corner3Criteria)
    }

    fun isCorner4(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.corner4Criteria)
    }

    fun isCross(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.crossCriteria)
    }

    fun isK(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.KCriteria)
    }

    fun isInverseK(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.inverseKCriteria)
    }

    fun isT(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.TCriteria)
    }

    fun isInverseT(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.inverseTCriteria)
    }

    fun isNormalCorner(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.normalCornerCriteria)
    }

    fun isNormalCorner(x: Int, y: Int): Boolean {
        return isNormalCorner(Cell(x, y))
    }

    fun isRoundCorner(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.roundCornerCriteria)
    }

    fun isRoundCorner(x: Int, y: Int): Boolean {
        return isRoundCorner(Cell(x, y))
    }

    fun isIntersection(cell: Cell): Boolean {
        return matchesAny(cell, GridPatternGroup.intersectionCriteria)
    }

    fun isIntersection(x: Int, y: Int): Boolean {
        return isIntersection(Cell(x, y))
    }

    fun copyCellsTo(cells: CellSet, grid: TextGrid) {
        val it: Iterator<*> = cells.iterator()
        while (it.hasNext()) {
            val cell = it.next() as Cell
            grid[cell] = this[cell]
        }
    }

    fun equals(grid: TextGrid): Boolean {
        if ((grid.height != height
                    || grid.width != width)
        ) {
            return false
        }
        val height = grid.height
        for (i in 0 until height) {
            val row1 = getRow(i).toString()
            val row2 = grid.getRow(i).toString()
            if (row1 != row2) return false
        }
        return true
    }

    /**
     * Fills all the cells in `cells` with `c`
     *
     * @param cells
     * @param c
     */
    fun fillCellsWith(cells: Iterable<Cell>, c: Char) {
        val it: Iterator<Cell> = cells.iterator()
        while (it.hasNext()) {
            val cell = it.next()
            set(cell.x, cell.y, c)
        }
    }

    /**
     *
     * Fills the continuous area with if c1 characters with c2,
     * flooding from cell x, y
     *
     * @param x
     * @param y
     * @param c1 the character to replace
     * @param c2 the character to replace c1 with
     * @return the list of cells filled
     */
    //	public CellSet fillContinuousArea(int x, int y, char c1, char c2){
    //		CellSet cells = new CellSet();
    //		//fillContinuousArea_internal(x, y, c1, c2, cells);
    //		seedFill(new Cell(x, y), c1, c2);
    //		return cells;
    //	}
    fun fillContinuousArea(x: Int, y: Int, c: Char): CellSet {
        return fillContinuousArea(Cell(x, y), c)
    }

    fun fillContinuousArea(cell: Cell, c: Char): CellSet {
        if (isOutOfBounds(cell)) throw IllegalArgumentException("Attempted to fill area out of bounds: $cell")
        return seedFillOld(cell, c)
    }

    private fun seedFill(seed: Cell, newChar: Char): CellSet {
        val cellsFilled = CellSet()
        val oldChar = get(seed)
        if (oldChar == newChar) return cellsFilled
        if (isOutOfBounds(seed)) return cellsFilled
        val stack = Stack<Cell>()
        stack.push(seed)
        while (!stack.isEmpty()) {
            val cell = stack.pop() as Cell

            //set(cell, newChar);
            cellsFilled.add(cell)
            val nCell = cell.north
            val sCell = cell.south
            val eCell = cell.east
            val wCell = cell.west
            if (get(nCell) == oldChar && !cellsFilled.contains(nCell)) stack.push(nCell)
            if (get(sCell) == oldChar && !cellsFilled.contains(sCell)) stack.push(sCell)
            if (get(eCell) == oldChar && !cellsFilled.contains(eCell)) stack.push(eCell)
            if (get(wCell) == oldChar && !cellsFilled.contains(wCell)) stack.push(wCell)
        }
        return cellsFilled
    }

    private fun seedFillOld(seed: Cell, newChar: Char): CellSet {
        val cellsFilled = CellSet()
        val oldChar = get(seed)
        if (oldChar == newChar) return cellsFilled
        if (isOutOfBounds(seed)) return cellsFilled
        val stack = Stack<Cell>()
        stack.push(seed)
        while (!stack.isEmpty()) {
            val cell = stack.pop() as Cell
            set(cell, newChar)
            cellsFilled.add(cell)
            val nCell = cell.north
            val sCell = cell.south
            val eCell = cell.east
            val wCell = cell.west
            if (get(nCell) == oldChar) stack.push(nCell)
            if (get(sCell) == oldChar) stack.push(sCell)
            if (get(eCell) == oldChar) stack.push(eCell)
            if (get(wCell) == oldChar) stack.push(wCell)
        }
        return cellsFilled
    }

    /**
     *
     * Locates and returns the '*' boundaries that we would
     * encounter if we did a flood-fill at `seed`.
     *
     * @param seed
     * @return
     */
    fun findBoundariesExpandingFrom(seed: Cell): CellSet {
        val boundaries = CellSet()
        val oldChar = get(seed)
        if (isOutOfBounds(seed)) return boundaries
        val newChar = 1.toChar() //TODO: kludge
        val stack = Stack<Cell>()
        stack.push(seed)
        while (!stack.isEmpty()) {
            val cell = stack.pop() as Cell
            set(cell, newChar)
            val nCell = cell.north
            val sCell = cell.south
            val eCell = cell.east
            val wCell = cell.west
            if (get(nCell) == oldChar) stack.push(nCell) else if (get(nCell) == '*') boundaries.add(
                nCell
            )
            if (get(sCell) == oldChar) stack.push(sCell) else if (get(sCell) == '*') boundaries.add(
                sCell
            )
            if (get(eCell) == oldChar) stack.push(eCell) else if (get(eCell) == '*') boundaries.add(
                eCell
            )
            if (get(wCell) == oldChar) stack.push(wCell) else if (get(wCell) == '*') boundaries.add(
                wCell
            )
        }
        return boundaries
    }

    //TODO: incomplete method seedFillLine()
    private fun seedFillLine(cell: Cell, newChar: Char): CellSet {
        val cellsFilled = CellSet()
        val stack = Stack<LineSegment>()
        val oldChar = get(cell)
        if (oldChar == newChar) return cellsFilled
        if (isOutOfBounds(cell)) return cellsFilled
        stack.push(LineSegment(cell.x, cell.x, cell.y, 1))
        stack.push(LineSegment(cell.x, cell.x, cell.y + 1, -1))
        var left: Int
        while (!stack.isEmpty()) {
            val segment = stack.pop() as LineSegment
            //expand to the left
            var x: Int = segment.x1
            while (x >= 0 && get(x, segment.y) == oldChar) {
                set(x, segment.y, newChar)
                cellsFilled.add(Cell(x, segment.y))
                --x
            }
            left = cell.east.x
            var skip = (x > segment.x1)
            if (left < segment.x1) { //leak on left?
                //TODO: i think the first param should be x
                stack.push( //new LineSegment(segment.y, left, segment.x1 - 1, -segment.dy));
                    LineSegment(x, left, segment.y - 1, -segment.dy)
                )
            }
            x = segment.x1 + 1
            do {
                if (!skip) {
                    while (x < width && get(x, segment.y) == oldChar) {
                        set(x, segment.y, newChar)
                        cellsFilled.add(Cell(x, segment.y))
                        ++x
                    }
                    stack.push(LineSegment(left, x - 1, segment.y, segment.dy))
                    if (x > segment.x2 + 1) //leak on right?
                        stack.push(LineSegment(segment.x2 + 1, x - 1, segment.y, -segment.dy))
                }
                skip = false //skip only once
                ++x
                while (x <= segment.x2 && get(x, segment.y) != oldChar) {
                    ++x
                }
                left = x
            } while (x < segment.x2)
        }
        return cellsFilled
    }

    fun cellContainsDashedLineChar(cell: Cell): Boolean {
        val c = get(cell)
        return dashedLines.contains(c)
    }


    @Throws(UnsupportedEncodingException::class)
    fun initialiseWithText(text: String, options: ProcessingOptions?=null): Boolean {
        val lines = ArrayList<StringBuffer>()
        val linesArray = text.split("(\r)?\n".toRegex()).dropLastWhile { it.isEmpty() }
            .toTypedArray()
        for (i in linesArray.indices) lines.add(StringBuffer(linesArray[i]))
        return initialiseWithLines(lines, options)
    }

    @Throws(UnsupportedEncodingException::class)
    fun initialiseWithLines(lines: ArrayList<StringBuffer>, options: ProcessingOptions?=null): Boolean {

        //remove blank rows at the bottom
        var done = false
        var i: Int = lines.size - 1
        while (!done) {
            val row = lines[i]
            if (row.isNotBlank()) done = true
            i--
        }
        rows = ArrayList(lines.subList(0, i + 2))
        if (options != null) fixTabs(options.tabSize) else fixTabs(ProcessingOptions.DEFAULT_TAB_SIZE)


        // make all lines of equal length
        // add blank outline around the buffer to prevent fill glitch
        // convert tabs to spaces (or remove them if setting is 0)
        val blankBorderSize = 2
        var maxLength = 0
        var index = 0
        var encoding: String? = null
        if (options != null) encoding = options.characterEncoding
        var it: Iterator<StringBuffer> = rows.iterator()
        while (it.hasNext()) {
            var row = it.next().toString()
            if (encoding != null) {
                val bytes = row.toByteArray()
                row = String(bytes, Charset.forName(encoding))
            }
            if (row.length > maxLength) maxLength = row.length
            rows[index] = StringBuffer(row)
            index++
        }
        it = rows.iterator()
        val newRows = ArrayList<StringBuffer>()
        //TODO: make the following depend on blankBorderSize
        val topBottomRow: StringBuffer =
            StringBuffer(" ".repeat( maxLength + blankBorderSize * 2))
        newRows.add(topBottomRow)
        newRows.add(topBottomRow)
        while (it.hasNext()) {
            val row = it.next()
            if (row.length < maxLength) {
                val borderString: String = " ".repeat( blankBorderSize)
                val newRow = StringBuffer()
                newRow.append(borderString)
                newRow.append(row)
                newRow.append(" ".repeat( maxLength - row.length))
                newRow.append(borderString)
                newRows.add(newRow)
            } else { //TODO: why is the following line like that?
                newRows.add(StringBuffer("  ").append(row).append("  "))
            }
        }
        //TODO: make the following depend on blankBorderSize
        newRows.add(topBottomRow)
        newRows.add(topBottomRow)
        rows = newRows
        replaceBullets()
        replaceHumanColorCodes()
        return true
    }

    private fun fixTabs(tabSize: Int) {
        var rowIndex = 0
        val it: Iterator<StringBuffer> = rows.iterator()
        while (it.hasNext()) {
            val row = it.next().toString()
            val newRow = StringBuffer()
            val chars = row.toCharArray()
            for (i in chars.indices) {
                if (chars[i] == '\t') {
                    val spacesLeft = tabSize - newRow.length % tabSize
                    if (DEBUG) {
                        println("Found tab. Spaces left: $spacesLeft")
                    }
                    val spaces: String = " ".repeat(spacesLeft)
                    newRow.append(spaces)
                } else {
                    val character = chars[i].toString()
                    newRow.append(character)
                }
            }
            rows[rowIndex] = newRow
            rowIndex++
        }
    }

    inner class CellColorPair(var cell: Cell, color: Int) {
        var color: Int

        init {
            this.color = color
        }
    }

    inner class CellStringPair(var cell: Cell, var string: String?)
    inner class CellTagPair(var cell: Cell, var tag: String)

    inner class Cell(var x: Int, var y: Int) {
        constructor(cell: Cell) : this(cell.x, cell.y) {}

        val north: Cell
            get() = Cell(x, y - 1)
        val south: Cell
            get() = Cell(x, y + 1)
        val east: Cell
            get() = Cell(x + 1, y)
        val west: Cell
            get() = Cell(x - 1, y)
        val nW: Cell
            get() = Cell(x - 1, y - 1)
        val nE: Cell
            get() = Cell(x + 1, y - 1)
        val sW: Cell
            get() = Cell(x - 1, y + 1)
        val sE: Cell
            get() = Cell(x + 1, y + 1)
        val neighbours4: CellSet
            get() {
                val result = CellSet()
                result.add(north)
                result.add(south)
                result.add(west)
                result.add(east)
                return result
            }
        val neighbours8: CellSet
            get() {
                val result = CellSet()
                result.add(north)
                result.add(south)
                result.add(west)
                result.add(east)
                result.add(nW)
                result.add(nE)
                result.add(sW)
                result.add(sE)
                return result
            }

        fun isNorthOf(cell: Cell): Boolean {
            return y < cell.y
        }

        fun isSouthOf(cell: Cell): Boolean {
            return y > cell.y
        }

        fun isWestOf(cell: Cell): Boolean {
            return x < cell.x
        }

        fun isEastOf(cell: Cell): Boolean {
            return x > cell.x
        }

        override fun equals(other: Any?): Boolean {
            val cell = other as Cell? ?: return false
            return x == cell.x && y == cell.y
        }

        override fun hashCode(): Int {
            return (x shl 16) or y
        }

        fun isNextTo(x2: Int, y2: Int): Boolean {
            if (abs(x2 - x) == 1 && abs(y2 - y) == 1) return false
            if (abs(x2 - x) == 1 && y2 == y) return true
            return abs(y2 - y) == 1 && x2 == x
        }

        fun isNextTo(cell: Cell): Boolean {
            return this.isNextTo(cell.x, cell.y)
        }

        override fun toString(): String {
            return "($x, $y)"
        }

        fun scale(s: Int) {
            x *= s
            y *= s
        }
    }

    private inner class LineSegment(var x1: Int, var x2: Int, var y: Int, var dy: Int)

    companion object {
        private val DEBUG = false
        private val boundaries = charArrayOf('/', '\\', '|', '-', '*', '=', ':')
        private val undisputableBoundaries = charArrayOf('|', '-', '*', '=', ':')
        private val horizontalLines = charArrayOf('-', '=')
        private val verticalLines = charArrayOf('|', ':')
        private val arrowHeads = charArrayOf('<', '>', '^', 'v', 'V')
        private val cornerChars = charArrayOf('\\', '/', '+')
        private val pointMarkers = charArrayOf('*')
        private val dashedLines = charArrayOf(':', '~', '=')
        private val entryPoints1 = charArrayOf('\\')
        private val entryPoints2 = charArrayOf('|', ':', '+', '\\', '/')
        private val entryPoints3 = charArrayOf('/')
        private val entryPoints4 = charArrayOf('-', '=', '+', '\\', '/')
        private val entryPoints5 = charArrayOf('\\')
        private val entryPoints6 = charArrayOf('|', ':', '+', '\\', '/')
        private val entryPoints7 = charArrayOf('/')
        private val entryPoints8 = charArrayOf('-', '=', '+', '\\', '/')
        private val humanColorCodes = HashMap<String, String>()

        val entryMap:Map<Char,Char> = mapOf(
            '┌' to  '+',
            '┐' to  '+',
            '└' to  '+',
            '┘' to  '+',
            '─' to  '-',
            '►' to  '>',
            '\'' to  '.',
            '`' to  '.',
            'V' to  'v'
        )
        init {
            humanColorCodes["GRE"] = "9D9"
            humanColorCodes["BLU"] = "55B"
            humanColorCodes["PNK"] = "FAA"
            humanColorCodes["RED"] = "E32"
            humanColorCodes["YEL"] = "FF3"
            humanColorCodes["BLK"] = "000"
        }

        private val markupTags = HashSet<String>()

        init {
            markupTags.add("d")
            markupTags.add("s")
            markupTags.add("io")
            markupTags.add("c")
            markupTags.add("mo")
            markupTags.add("tr")
            markupTags.add("o")
        }

        fun makeSameSizeAs(grid: TextGrid): TextGrid {
            return TextGrid(grid.width, grid.height)
        }

        fun isBoundary(c: Char): Boolean {
            return boundaries.contains(c)
        }

        fun isHorizontalLine(c: Char): Boolean {
            return horizontalLines.contains(c)
        }

        fun isVerticalLine(c: Char): Boolean {
            return verticalLines.contains(c)
        }
    }
}

